package pgproto3

import (
	"bytes"
	"encoding/binary"
	"strconv"
)

type ErrorResponse struct {
	Severity            string
	SeverityUnlocalized string // only in 9.6 and greater
	Code                string
	Message             string
	Detail              string
	Hint                string
	Position            int32
	InternalPosition    int32
	InternalQuery       string
	Where               string
	SchemaName          string
	TableName           string
	ColumnName          string
	DataTypeName        string
	ConstraintName      string
	File                string
	Line                int32
	Routine             string

	UnknownFields map[byte]string
}

// Backend identifies this message as sendable by the PostgreSQL backend.
func (*ErrorResponse) Backend() {}

// Decode decodes src into dst. src must contain the complete message with the exception of the initial 1 byte message
// type identifier and 4 byte message length.
func (dst *ErrorResponse) Decode(src []byte) error {
	*dst = ErrorResponse{}

	buf := bytes.NewBuffer(src)

	for {
		k, err := buf.ReadByte()
		if err != nil {
			return err
		}
		if k == 0 {
			break
		}

		vb, err := buf.ReadBytes(0)
		if err != nil {
			return err
		}
		v := string(vb[:len(vb)-1])

		switch k {
		case 'S':
			dst.Severity = v
		case 'V':
			dst.SeverityUnlocalized = v
		case 'C':
			dst.Code = v
		case 'M':
			dst.Message = v
		case 'D':
			dst.Detail = v
		case 'H':
			dst.Hint = v
		case 'P':
			s := v
			n, _ := strconv.ParseInt(s, 10, 32)
			dst.Position = int32(n)
		case 'p':
			s := v
			n, _ := strconv.ParseInt(s, 10, 32)
			dst.InternalPosition = int32(n)
		case 'q':
			dst.InternalQuery = v
		case 'W':
			dst.Where = v
		case 's':
			dst.SchemaName = v
		case 't':
			dst.TableName = v
		case 'c':
			dst.ColumnName = v
		case 'd':
			dst.DataTypeName = v
		case 'n':
			dst.ConstraintName = v
		case 'F':
			dst.File = v
		case 'L':
			s := v
			n, _ := strconv.ParseInt(s, 10, 32)
			dst.Line = int32(n)
		case 'R':
			dst.Routine = v

		default:
			if dst.UnknownFields == nil {
				dst.UnknownFields = make(map[byte]string)
			}
			dst.UnknownFields[k] = v
		}
	}

	return nil
}

// Encode encodes src into dst. dst will include the 1 byte message type identifier and the 4 byte message length.
func (src *ErrorResponse) Encode(dst []byte) []byte {
	return append(dst, src.marshalBinary('E')...)
}

func (src *ErrorResponse) marshalBinary(typeByte byte) []byte {
	var bigEndian BigEndianBuf
	buf := &bytes.Buffer{}

	buf.WriteByte(typeByte)
	buf.Write(bigEndian.Uint32(0))

	if src.Severity != "" {
		buf.WriteByte('S')
		buf.WriteString(src.Severity)
		buf.WriteByte(0)
	}
	if src.SeverityUnlocalized != "" {
		buf.WriteByte('V')
		buf.WriteString(src.SeverityUnlocalized)
		buf.WriteByte(0)
	}
	if src.Code != "" {
		buf.WriteByte('C')
		buf.WriteString(src.Code)
		buf.WriteByte(0)
	}
	if src.Message != "" {
		buf.WriteByte('M')
		buf.WriteString(src.Message)
		buf.WriteByte(0)
	}
	if src.Detail != "" {
		buf.WriteByte('D')
		buf.WriteString(src.Detail)
		buf.WriteByte(0)
	}
	if src.Hint != "" {
		buf.WriteByte('H')
		buf.WriteString(src.Hint)
		buf.WriteByte(0)
	}
	if src.Position != 0 {
		buf.WriteByte('P')
		buf.WriteString(strconv.Itoa(int(src.Position)))
		buf.WriteByte(0)
	}
	if src.InternalPosition != 0 {
		buf.WriteByte('p')
		buf.WriteString(strconv.Itoa(int(src.InternalPosition)))
		buf.WriteByte(0)
	}
	if src.InternalQuery != "" {
		buf.WriteByte('q')
		buf.WriteString(src.InternalQuery)
		buf.WriteByte(0)
	}
	if src.Where != "" {
		buf.WriteByte('W')
		buf.WriteString(src.Where)
		buf.WriteByte(0)
	}
	if src.SchemaName != "" {
		buf.WriteByte('s')
		buf.WriteString(src.SchemaName)
		buf.WriteByte(0)
	}
	if src.TableName != "" {
		buf.WriteByte('t')
		buf.WriteString(src.TableName)
		buf.WriteByte(0)
	}
	if src.ColumnName != "" {
		buf.WriteByte('c')
		buf.WriteString(src.ColumnName)
		buf.WriteByte(0)
	}
	if src.DataTypeName != "" {
		buf.WriteByte('d')
		buf.WriteString(src.DataTypeName)
		buf.WriteByte(0)
	}
	if src.ConstraintName != "" {
		buf.WriteByte('n')
		buf.WriteString(src.ConstraintName)
		buf.WriteByte(0)
	}
	if src.File != "" {
		buf.WriteByte('F')
		buf.WriteString(src.File)
		buf.WriteByte(0)
	}
	if src.Line != 0 {
		buf.WriteByte('L')
		buf.WriteString(strconv.Itoa(int(src.Line)))
		buf.WriteByte(0)
	}
	if src.Routine != "" {
		buf.WriteByte('R')
		buf.WriteString(src.Routine)
		buf.WriteByte(0)
	}

	for k, v := range src.UnknownFields {
		buf.WriteByte(k)
		buf.WriteByte(0)
		buf.WriteString(v)
		buf.WriteByte(0)
	}

	buf.WriteByte(0)

	binary.BigEndian.PutUint32(buf.Bytes()[1:5], uint32(buf.Len()-1))

	return buf.Bytes()
}
